﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using HandyControl.Data;
using HandyControl.Expression.Drawing;
using HandyControl.Tools;
using HandyControl.Tools.Interop;

namespace HandyControl.Controls;

public class GlowWindow : Window
{
    internal int DeferGlowChangesCount;

    private readonly GlowEdge[] _glowEdges = new GlowEdge[4];

    private DispatcherTimer _makeGlowVisibleTimer;

    private bool _isGlowVisible;

    private bool _useLogicalSizeForRestore;

    private bool _updatingZOrder;

    private Rect _logicalSizeForRestore = Rect.Empty;

    public static readonly DependencyProperty ActiveGlowColorProperty = DependencyProperty.Register(
        nameof(ActiveGlowColor), typeof(Color), typeof(GlowWindow), new PropertyMetadata(default(Color), OnGlowColorChanged));

    public Color ActiveGlowColor
    {
        get => (Color) GetValue(ActiveGlowColorProperty);
        set => SetValue(ActiveGlowColorProperty, value);
    }

    public static readonly DependencyProperty InactiveGlowColorProperty = DependencyProperty.Register(
        nameof(InactiveGlowColor), typeof(Color), typeof(GlowWindow), new PropertyMetadata(default(Color), OnGlowColorChanged));

    public Color InactiveGlowColor
    {
        get => (Color) GetValue(InactiveGlowColorProperty);
        set => SetValue(InactiveGlowColorProperty, value);
    }

    #region internal

    internal void EndDeferGlowChanges()
    {
        foreach (var current in LoadedGlowWindows) current.CommitChanges();
    }

    #endregion

    #region protected

    protected virtual bool ShouldShowGlow
    {
        get
        {
            var handle = this.GetHandle();
            return InteropMethods.IsWindowVisible(handle) && !InteropMethods.IsIconic(handle) &&
                   !InteropMethods.IsZoomed(handle) && ResizeMode != ResizeMode.NoResize;
        }
    }

    private IntPtr HwndSourceHook(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg <= InteropValues.WM_WINDOWPOSCHANGED)
        {
            if (msg == InteropValues.WM_ACTIVATE)
            {
                return IntPtr.Zero;
            }

            if (msg != InteropValues.WM_QUIT)
            {
                switch (msg)
                {
                    case InteropValues.WM_WINDOWPOSCHANGING:
                        WmWindowPosChanging(lParam);
                        return IntPtr.Zero;
                    case InteropValues.WM_WINDOWPOSCHANGED:
                        WmWindowPosChanged(lParam);
                        return IntPtr.Zero;
                    default:
                        return IntPtr.Zero;
                }
            }
        }
        else
        {
            if (msg <= InteropValues.WM_NCRBUTTONDBLCLK)
            {
                switch (msg)
                {
                    case InteropValues.WM_SETICON:
                        break;
                    case InteropValues.WM_NCCREATE:
                    case InteropValues.WM_NCDESTROY:
                        return IntPtr.Zero;
                    case InteropValues.WM_NCACTIVATE:
                        handled = true;
                        return WmNcActivate(hWnd, wParam);
                    default:
                        switch (msg)
                        {
                            case InteropValues.WM_NCRBUTTONDOWN:
                            case InteropValues.WM_NCRBUTTONUP:
                            case InteropValues.WM_NCRBUTTONDBLCLK:
                                handled = true;
                                return IntPtr.Zero;
                            default:
                                return IntPtr.Zero;
                        }
                }
            }
            else
            {
                switch (msg)
                {
                    case InteropValues.WM_NCUAHDRAWCAPTION:
                    case InteropValues.WM_NCUAHDRAWFRAME:
                        handled = true;
                        return IntPtr.Zero;
                    default:
                        if (msg != InteropValues.WM_SYSCOMMAND) return IntPtr.Zero;
                        WmSysCommand(hWnd, wParam);
                        return IntPtr.Zero;
                }
            }
        }

        handled = true;
        return CallDefWindowProcWithoutVisibleStyle(hWnd, msg, wParam, lParam);
    }

    protected override void OnActivated(EventArgs e)
    {
        UpdateGlowActiveState();
        base.OnActivated(e);
    }

    protected override void OnDeactivated(EventArgs e)
    {
        UpdateGlowActiveState();
        base.OnDeactivated(e);
    }

    protected override void OnClosed(EventArgs e)
    {
        StopTimer();
        DestroyGlowWindows();
        base.OnClosed(e);
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        var hwndSource = this.GetHwndSource();
        if (hwndSource != null)
        {
            hwndSource.AddHook(HwndSourceHook);
            CreateGlowWindowHandles();
        }
    }

    #endregion

    #region private

    private IEnumerable<GlowEdge> LoadedGlowWindows => from w in _glowEdges where w != null select w;

    private bool IsGlowVisible
    {
        get => _isGlowVisible;
        set
        {
            if (_isGlowVisible != value)
            {
                _isGlowVisible = value;
                for (var i = 0; i < _glowEdges.Length; i++)
                {
                    GetOrCreateGlowWindow(i).IsVisible = value;
                }
            }
        }
    }

    private GlowEdge GetOrCreateGlowWindow(int direction)
    {
        return _glowEdges[direction] ?? (_glowEdges[direction] = new GlowEdge(this, (Dock) direction)
        {
            ActiveGlowColor = ActiveGlowColor,
            InactiveGlowColor = InactiveGlowColor,
            IsActive = IsActive
        });
    }

    private static void OnResizeModeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var customChromeWindow = (GlowWindow) obj;
        customChromeWindow.UpdateGlowVisibility(false);
    }

    private static void OnGlowColorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) => ((GlowWindow) obj).UpdateGlowColors();

    private void UpdateGlowColors()
    {
        using (DeferGlowChanges())
        {
            foreach (var current in LoadedGlowWindows)
            {
                current.ActiveGlowColor = ActiveGlowColor;
                current.InactiveGlowColor = InactiveGlowColor;
            }
        }
    }

    private void UpdateGlowVisibility(bool delayIfNecessary)
    {
        var shouldShowGlow = ShouldShowGlow;
        if (shouldShowGlow != IsGlowVisible)
        {
            if (SystemParameters.MinimizeAnimation && shouldShowGlow && delayIfNecessary)
            {
                if (_makeGlowVisibleTimer != null)
                {
                    _makeGlowVisibleTimer.Stop();
                }
                else
                {
                    _makeGlowVisibleTimer = new DispatcherTimer
                    {
                        Interval = TimeSpan.FromMilliseconds(200.0)
                    };
                    _makeGlowVisibleTimer.Tick += OnDelayedVisibilityTimerTick;
                }

                _makeGlowVisibleTimer.Start();
                return;
            }

            StopTimer();
            IsGlowVisible = shouldShowGlow;
        }
    }

    private void StopTimer()
    {
        if (_makeGlowVisibleTimer != null)
        {
            _makeGlowVisibleTimer.Stop();
            _makeGlowVisibleTimer.Tick -= OnDelayedVisibilityTimerTick;
            _makeGlowVisibleTimer = null;
        }
    }

    private void OnDelayedVisibilityTimerTick(object sender, EventArgs e)
    {
        StopTimer();
        UpdateGlowWindowPositions(false);
    }

    private void UpdateGlowWindowPositions(bool delayIfNecessary)
    {
        using (DeferGlowChanges())
        {
            UpdateGlowVisibility(delayIfNecessary);
            foreach (var current in LoadedGlowWindows) current.UpdateWindowPos();
        }
    }

    private IDisposable DeferGlowChanges() => new ChangeScope(this);

    private void UpdateGlowActiveState()
    {
        using (DeferGlowChanges())
        {
            foreach (var current in LoadedGlowWindows)
            {
                current.IsActive = IsActive;
            }
        }
    }

    private void DestroyGlowWindows()
    {
        for (var i = 0; i < _glowEdges.Length; i++)
        {
            using (_glowEdges[i])
            {
                _glowEdges[i] = null;
            }
        }
    }

    private void WmWindowPosChanging(IntPtr lParam)
    {
        var windowpos = (InteropValues.WINDOWPOS) Marshal.PtrToStructure(lParam, typeof(InteropValues.WINDOWPOS));
        if ((windowpos.flags & 2u) == 0u && (windowpos.flags & 1u) == 0u && windowpos.cx > 0 && windowpos.cy > 0)
        {
            var rect = new Rect(windowpos.x, windowpos.y, windowpos.cx, windowpos.cy);
            rect = rect.DeviceToLogicalUnits();
            if (_useLogicalSizeForRestore)
            {
                rect = _logicalSizeForRestore;
                _logicalSizeForRestore = Rect.Empty;
                _useLogicalSizeForRestore = false;
            }

            var logicalRect = GetOnScreenPosition(rect);
            logicalRect = logicalRect.LogicalToDeviceUnits();
            windowpos.x = (int) logicalRect.X;
            windowpos.y = (int) logicalRect.Y;
            Marshal.StructureToPtr(windowpos, lParam, true);
        }
    }

    private void UpdateZOrderOfOwner(IntPtr hwndOwner)
    {
        var lastOwnedWindow = IntPtr.Zero;
        InteropMethods.EnumThreadWindows(InteropMethods.GetCurrentThreadId(), delegate (IntPtr hwnd, IntPtr lParam)
        {
            if (InteropMethods.GetWindow(hwnd, 4) == hwndOwner) lastOwnedWindow = hwnd;
            return true;
        }, IntPtr.Zero);

        if (lastOwnedWindow != IntPtr.Zero && InteropMethods.GetWindow(hwndOwner, 3) != lastOwnedWindow)
            InteropMethods.SetWindowPos(hwndOwner, lastOwnedWindow, 0, 0, 0, 0, 19);
    }

    private void UpdateZOrderOfThisAndOwner()
    {
        if (_updatingZOrder) return;
        try
        {
            _updatingZOrder = true;
            var windowInteropHelper = new WindowInteropHelper(this);
            var handle = windowInteropHelper.Handle;
            foreach (var current in LoadedGlowWindows)
            {
                var window = InteropMethods.GetWindow(current.Handle, 3);
                if (window != handle) InteropMethods.SetWindowPos(current.Handle, handle, 0, 0, 0, 0, 19);
                handle = current.Handle;
            }

            var owner = windowInteropHelper.Owner;
            if (owner != IntPtr.Zero) UpdateZOrderOfOwner(owner);
        }
        finally
        {
            _updatingZOrder = false;
        }
    }

    private void WmWindowPosChanged(IntPtr lParam)
    {
        try
        {
            var windowpos = (InteropValues.WINDOWPOS) Marshal.PtrToStructure(lParam, typeof(InteropValues.WINDOWPOS));

            UpdateGlowWindowPositions((windowpos.flags & 64u) == 0u);
            UpdateZOrderOfThisAndOwner();
        }
        catch
        {
            // ignored
        }
    }

    private Rect GetOnScreenPosition(Rect floatRect)
    {
        var result = floatRect;
        floatRect = floatRect.LogicalToDeviceUnits();
        ScreenHelper.FindMaximumSingleMonitorRectangle(floatRect, out _, out var rect2);
        if (!floatRect.IntersectsWith(rect2))
        {
            ScreenHelper.FindMonitorRectsFromPoint(InteropMethods.GetCursorPos(), out _, out rect2);
            rect2 = rect2.DeviceToLogicalUnits();
            if (result.Width > rect2.Width) result.Width = rect2.Width;
            if (result.Height > rect2.Height) result.Height = rect2.Height;
            if (rect2.Right <= result.X) result.X = rect2.Right - result.Width;
            if (rect2.Left > result.X + result.Width) result.X = rect2.Left;
            if (rect2.Bottom <= result.Y) result.Y = rect2.Bottom - result.Height;
            if (rect2.Top > result.Y + result.Height) result.Y = rect2.Top;
        }

        return result;
    }

    private static InteropValues.MONITORINFO MonitorInfoFromWindow(IntPtr hWnd)
    {
        var hMonitor = InteropMethods.MonitorFromWindow(hWnd, 2);
        var result = default(InteropValues.MONITORINFO);
        result.cbSize = (uint) Marshal.SizeOf(typeof(InteropValues.MONITORINFO));
        InteropMethods.GetMonitorInfo(hMonitor, ref result);
        return result;
    }

    private IntPtr WmNcActivate(IntPtr hWnd, IntPtr wParam) => InteropMethods.DefWindowProc(hWnd, InteropValues.WM_NCACTIVATE, wParam, InteropMethods.HRGN_NONE);

    private bool IsAeroSnappedToMonitor(IntPtr hWnd)
    {
        var monitorinfo = MonitorInfoFromWindow(hWnd);
        var logicalRect = new Rect(Left, Top, Width, Height);
        logicalRect = logicalRect.LogicalToDeviceUnits();
        return MathHelper.AreClose(monitorinfo.rcWork.Height, logicalRect.Height) && MathHelper.AreClose(monitorinfo.rcWork.Top, logicalRect.Top);
    }

    private void WmSysCommand(IntPtr hWnd, IntPtr wParam)
    {
        var num = InteropMethods.GET_SC_WPARAM(wParam);

        if (num == InteropValues.SC_MOVE)
        {
            InteropMethods.RedrawWindow(hWnd, IntPtr.Zero, IntPtr.Zero,
                InteropValues.RedrawWindowFlags.Invalidate | InteropValues.RedrawWindowFlags.NoChildren |
                InteropValues.RedrawWindowFlags.UpdateNow | InteropValues.RedrawWindowFlags.Frame);
        }

        if ((num == InteropValues.SC_MAXIMIZE || num == InteropValues.SC_MINIMIZE || num == InteropValues.SC_MOVE ||
             num == InteropValues.SC_SIZE) && WindowState == WindowState.Normal && !IsAeroSnappedToMonitor(hWnd))
        {
            _logicalSizeForRestore = new Rect(Left, Top, Width, Height);
        }

        if (num == InteropValues.SC_MOVE && WindowState == WindowState.Maximized && _logicalSizeForRestore == Rect.Empty)
        {
            _logicalSizeForRestore = new Rect(Left, Top, Width, Height);
        }

        if (num == InteropValues.SC_RESTORE && WindowState != WindowState.Minimized &&
            _logicalSizeForRestore.Width > 0.0 && _logicalSizeForRestore.Height > 0.0)
        {
            Left = _logicalSizeForRestore.Left;
            Top = _logicalSizeForRestore.Top;
            Width = _logicalSizeForRestore.Width;
            Height = _logicalSizeForRestore.Height;
            _useLogicalSizeForRestore = true;
        }
    }

    private IntPtr CallDefWindowProcWithoutVisibleStyle(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
    {
        var flag = VisualHelper.ModifyStyle(hWnd, InteropValues.WS_VISIBLE, 0);
        var result = InteropMethods.DefWindowProc(hWnd, msg, wParam, lParam);
        if (flag) VisualHelper.ModifyStyle(hWnd, 0, InteropValues.WS_VISIBLE);
        return result;
    }

    private void CreateGlowWindowHandles()
    {
        for (var i = 0; i < _glowEdges.Length; i++) GetOrCreateGlowWindow(i).EnsureHandle();
    }

    #endregion

}
